The equals method in Java. How to implement it correctly?

Radosław Kondziołka
Calendar icon
25 marca 2021

In Java, all classes inherit from the "java.lang.Object" class. Among the inherited methods is the "equals" method. Correct implementation of this method is crucial for the correctness of the program and - contrary to appearances - not necessarily trivial. Many data structures rely on its correct implementation. Therefore, a wrong implementation of it results in their incorrect behavior.

The java.lang.Object.equals method

The equals method allows you to determine whether two objects are equal. Its default definition provided by the Object class is based on object references. Such an implementation is sufficient in many cases. In general, for classes whose purpose is to provide some functionality, the equals method is unlikely to be implemented. An example of this is the HTTP client. It's hard to imagine at all how such objects could be compared other than by identity. It is in such cases that you should rely on the default implementation. The situation is different for objects representing entities from the modeled world, such as objects representing books, notes, etc. It is for these types of objects that the equals method is generally provided.

The correctness of the implementation of the title method can be considered in two ways:

  1. Objects should be equal to each other when in the modeled world they would be equal. For example, we can consider two books as equal if they have the same ISBN number.
  2. The equals method must meet the so-called contracts, which are required by the Java language standard and whose observance is necessary for the correct behavior of certain data structures.

Before we move on to analyze the aforementioned contracts, let's take a look at the simple class hierarchy we will refer to next.

1class Book {
2    String isbn;
3}
4
5class Ebook extends Book {
6    String format;
7}

Contracts against equals

The language standard requires equals implementations to maintain the following invariants:

  • maneuverability, i.e., the object is equal to itself. In other words, for any object o, it is true that o.equals(o) == true,
  • symmetry, that is, if the first object is equal to the second, then the second object is also equal to the first. This means that if o1.equals(o2) returns true (false) then o2.equals(o1) must also return true (false),
  • consistency, that is, for any two objects, the o1.equals(o2) method should always return the same value, as long as no changes have occurred in the objects,
  • transitivity is a condition that ensures that the result of the equals operation is transitive, i.e. if we have three objects o1, o2, o3, and if o1 is equal to o2 and o2 is equal to o3 then o1 is equal to o3,
  • comparing an object and a null value always returns false.

Correct implementation of the equals method

Let's now focus on the correct implementation of the equals method, i.e. one that preserves all the objections introduced by the language standard. In general, if we consider comparing objects of exactly the same type, the situation is simple and standard implementations, based on comparing object fields, are correct and sufficient. However, the situation is not so simple, because equals takes a parameter of type Object in its arguments:

1public boolean equals(Object o)

Consequently, an object of any other type can be compared to an instance of our class. And, while it is obvious that objects from different hierarchies of classes are simply different, the equality of objects remaining in the same hierarchy can already be considered.

Let's now focus on the classes presented above - the book and the ebook. Let's establish that we would like a situation in which ebook and book can be equal. Let's consider a simple implementation:

1class Book {
2    public boolean equals(Object o){
3        if(!(o instanceof Book)) {
4            return false;
5        }
6        return this.isbn == ((Book)o).isbn;
7    }
8}

This implementation recognizes that two books (and their derivatives - ebooks) are equal when their ISBN numbers are equal. A reasonable implementation for the Ebook class could look like this:

1class Ebook {
2    public boolean equals(Object o) {
3        if ((o instanceof Ebook)) {
4            return format.equals(((Ebook) o).format) && super.equals(o);
5        } else if ((o instanceof Book)) {
6            return super.equals(o);
7        }
8        return false;
9    }
10}

The implementation of Ebook.equals considers two cases:

  1. The object being compared has the type Ebook. The situation is simple - we are comparing objects of the same type.
  2. We compare an instance of Ebook with an instance of Book. To do this, we call a method from the superclass to compare the part that can be compared - only the ISBN code.

It's not hard to see that both methods provide maneuverability, symmetry and consistency. However, let's look at how the situation looks with transitivity. Well, equals implemented in this way is not transitive. To see this, just analyze the following case:

1Book b1 = new Book("1");
2Ebook e1 = new Ebook("1", "mobi"), e2 = new Ebook("1", "epub");
3e1.equals(b1) -> true   (1)
4b1.equals(e2) -> true   (2)
5e1.equals(e2) -> false  (3)

As we can see, operation (3) returns false, contrary to what would be expected from a transitive operator.

What about this transitive?

Note that an immediate, more general conclusion can be drawn from the example presented. Namely, that it is impossible to implement an equals method that would involve comparing objects in a parent-child relationship and at the same time be transitive. This state of affairs is not due to the limitations of the language, but is simply a direct consequence of inheritance. Well, the class that is higher in the class hierarchy has no idea about the fields that are in the class that is lower in the hierarchy. In our example, the book only knows about the ISBN number and can at most expect this number in the derived classes. In other words, when comparing a book and an ebook, there must be so-called logical object sliceing, i.e. treating the ebook as if it were an ordinary book. This is - let it ring out again - a natural consequence of comparing entities of different types, both in the real world and object world sense.

How then can such a problem be solved? In such a case, two things can be done:

  1. Prevent inheritance of classes that are so-called value classes (classes representing values or real world objects). As a matter of fact, this is quite reasonable both from the point of view of real world modeling and from the technical point of view - such a procedure greatly simplifies the implementation of equals.
  2. If for some reason our class has to be open to possible inheritance, we can consider that objects of different types are never equal to each other. Then the implementation is also very simple. The method template with this approach could look like this:
1public boolean equals(Object o) {
2    if(o == null) return false;
3    if(o.getClass() != this.getClass()) return false;
4    ... // just compare fields
5}

In this approach, note that such a method does not behave correctly for derived classes.

Let's go back to the source problem for a moment. Is it really impossible to implement the equals method so that it meets all the requirements and at the same time is able to compare objects from different levels in the hierarchy? In general, there are techniques that allow for a correct implementation. But then the question still remains, is it really reasonable that objects of different types can be equals? Secondly, such solutions are most often complicated and much more difficult to implement than one would expect from equals. As a result, however, it seems to make the most sense to consider that value-carrying classes should be classes that are closed to extension.

Summary

The implementation of the java equals method seems to be very simple. However, special attention should be paid to its proper implementation, as failure to meet its requirements can cause errors that will not necessarily be apparent at first glance. A programmer providing an java equals implementation should carefully analyze whether his function adheres to all the required strictures.

Read also

Calendar icon

28 marzec

RABATKA Action - Discover the spring promotion!

The annual spring promotion is underway: RABATKA Action, thanks to which you can save on open training from our proprietary offer.

Calendar icon

8 marzec

Don’t miss the opportunity - register for the IREB exploRE 2024 conference and meet the best experts in the field of requirements

We invite you to the IREB exploRE 2024 conference dedicated to requirements engineering and business analysis. The conference will ta...

Calendar icon

18 styczeń

How to prepare for the ISTQB® Foundation Level exam?

How toprepare for the ISTQB® Foundation Level exam? Check valuable tips before taking the test.